--Willy Byte in the Digital Dimension--
A 4am crack                  2021-08-04
---------------------------------------

Name: Willy Byte in the Digital
  Dimension
Genre: action
Year: 1984
Credits: Murray Krehbiel (programming)
  Greg Hammond (graphics)
Publisher: Data Trek
Platform: Apple ][ (48K)
Media: 5.25-inch disk
Sides: 2
OS: custom

 _____________________________________
{                                     }
{ McCoy: Lieutenant, you are looking  }
{    at the only Starfleet cadet who  }
{    ever beat the no-win scenario.   }
{ Saavik: How?                        }
{ Kirk: I reprogrammed the simulation }
{    so it was possible to rescue the }
{    ship.                            }
{ Saavik: What?                       }
{ David: He cheated.                  }
{ Kirk: I changed the conditions of   }
{    the test.                        }
{                    - "Star Trek II" }
{_____________________________________}


                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


The game boots side B, so we will start
there.

COPYA
  no errors on side B
  read error on first pass of side A

Locksmith Fast Disk Backup
  track 6 of side A is unreadable

EDD 4 bit copy (no sync, no count)
  no errors, but copy reboots just
  before level 1
  (after entering a word or selecting a
  difficulty level for the random word)

Copy ][+ nibble editor
  track 6 of side A looks normal at
  first glace

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
----------------------------------------

TRACK: 06  START: 1FDC  LENGTH: 17B7

1FB8: FF FF FF FF FF FF FF FF   VIEW
1FC0: FF FF FF FF FF FF FF FF
1FC8: FF FF FF FF FF FF FF FF
1FD0: FF FF FF FF FF FF FF FF
1FD8: FF FF FF FF D5 AA 96 FF  <-1FDC
1FE0: FE AA AF AA AA FF FB DE
1FE8: AA EB FF FF FF FF FF FF
1FF0: D5 AA AD 96 96 96 96 96
1FF8: 96 96 96 96 96 96 96 96

                 --^--

  Ah! But look closer at that address
  field:

                 --v--

1FD8: FF FF FF FF D5 AA 96 FF  <-1FDC
                  ^^^^^^^^
                  prologue

1FE0: FE AA AF AA AA FF FB DE
         ^^^^^ ^^^^^ ^^^^^
         track sectr chksm

                 --^--

  Decoding the 4-and-4 encoded values,
  this is sector 0 on track...5? But
  this is track 6! The address field is
  lying to me. Bad disk, no biscuit!

  However, this can't be the whole
  story, because EDD reported no errors
  but the protected backup failed. EDD
  should have no problem reproducing
  a track-claiming-to-be-another-track.
  So something else is going on.

  I've seen similar weirdness on disks
  from Electronic Arts (see 4am crack
  no. 1033 "Pinball Construction Set").
  They used an extra wide track that
  spanned track 5 through 6. (Yes, you
  read that correctly.) That makes me
  wonder if this disk is also using an
  extra wide track, and I think I know
  how I can find out: by looking at
  track 5.5.

                 --v--

TRACK: 05.50  START: 220D  LENGTH: 17B7

21E8: FF FF FF FF FF FF FF FF   VIEW
21F0: FF FF FF FF FF FF FF FF
21F8: FF FF FF FF FF FF FF FF
2200: FF FF FF FF FF FF FF FF
2208: FF FF FF FF FF D5 AA 96  <-220D
                     ^^^^^^^^
                     prologue

2210: FF FE AA AF AA AA FF FB
            ^^^^^ ^^^^^ ^^^^^
            T=$05 S=$00 chksm

2218: DE AA EB FF FF FF FF FF
2220: FF D5 AA AD 96 96 96 96
2228: 96 96 96 96 96 96 96 96

                 --^--

  Track 5.5 is exactly the same as
  track 6: 16 complete sectors, all
  claiming to be track 5.

Why didn't COPYA work?
  Track 6 is lying to us.

Why didn't Locksmith FDB work?
  Likewise

Why didn't my EDD copy work?
  Runtime protection check is somehow
  checking the extra wide track that
  spans tracks 5, 5.5, and 6 on side A.

Next steps:

  1. Find the protection check
  2. Disable it
  3. Go to the (home) gym

                   ~

               Chapter 1
    To Everything (Turn, Turn, Turn)
  There Is A Season (Turn, Turn, Turn)


I am working under the assumption that
side B is unprotected. It can be copied
by COPYA, and the copy boots and runs
and eventually says

         PLEASE TURN DISK OVER
         AND PRESS SPACE BAR.

So where is that, exactly.

Turning to my trusty sector editor, I
search for "BD 89 C0" (LDA $C089,X, one
common way to turn on the drive motor).
Side A has nothing at all; side B has
several matches but nothing that looks
suspicious.

Hmm. I really don't want to trace this
entire program from the boot sector.

I search for "AD 00 C0" (LDA $C000,
checks for a keypress), and I find a
plethora of matches on side B:

                 --v--

[$AD $00 $C0]
------------- DISK SEARCH -------------

$01/$0E-$3A   $03/$01-$5C   $09/$0C-$89
$09/$0C-$BC   $09/$0C-$F2   $09/$0D-$6C
$09/$0F-$79   $0A/$03-$3C   $0A/$03-$72
$0A/$03-$EC   $18/$06-$F1   $1B/$00-$04
$1D/$0F-$27   $21/$07-$DF   $22/$08-$44

                 --^--

Hmm. Let's search for part of the
message instead. Searching for "TURN"
in high bit ASCII finds nothing, but
re-searching in low bit ASCII finds two
matches:

                 --v--

["TURN"]
------------- DISK SEARCH -------------

$09/$0E-$48   $0A/$00-$94

                 --^--

Promising!

The match on track $09 looks like this:

                 --v--

-------------- DISK EDIT --------------
TRACK $09/SECTOR $0E/VOLUME $FE/BYTE$48
---------------------------------------
$00: 0A 8D E1 B7 A9 22 8D EC   ..a7)".l
$08: B7 A9 0F 8D ED B7 A9 69   7)..m7)i
$10: 8D F1 B7 4C 70 B7 8D 1A   .q7Lp7..
$18: 62 A9 80 20 A8 FC AD 30   b). (|-0
$20: C0 88 D0 F5 60 29 62 34   @.Pu`)b4
$28: 62 0F 60 CA CF D9 D3 D4   b.`JOYST
$30: C9 C3 CB 00 0F 60 CB C5   ICK..`KE
$38: D9 C2 CF C1 D2 C4 00 09   YBOARD..
$40: 88 50 4C 45 41 53 45 20   .PLEASE
$48:>54<55 52 4E 20 44 49 53   TURN DIS
$50: 4B 20 4F 56 45 52 00 09   K OVER..
$58: 90 41 4E 44 20 50 52 45   .AND PRE
$60: 53 53 20 53 50 41 43 45   SS SPACE
$68: 20 42 41 52 2E 00 00 50    BAR...P
$70: 44 4F 20 59 4F 55 20 57   DO YOU W
$78: 41 4E 54 20 54 4F 20 44   ANT TO D

                 --^--

That is definitely the message I see
before flipping to side A. And there
were several matches for the LDA $C000
instruction nearby. The one on track 9
looks like this:

                 --v--

[T09,S0D]
----------- DISASSEMBLY MODE ----------
; check for key
006C:AD 00 C0       LDA   $C000
006F:10 FB          BPL   $006C

; is it SPACE
0071:C9 A0          CMP   #$A0

; no, loop back
0073:D0 F7          BNE   $006C

; clear keyboard stroke
0075:8D 10 C0       STA   $C010

; turn on drive motor (I found this
; in my earlier search, but as you'll
; see, it's not suspicious)
0078:AE E9 B7       LDX   $B7E9
007B:BD 89 C0       LDA   $C089,X

; wait
007E:A9 00          LDA   #$00
0080:20 A8 FC       JSR   $FCA8

; check if disk is write-protected
0083:BD 8D C0       LDA   $C08D,X
0086:BD 8E C0       LDA   $C08E,X

; branch if it is not write-protected
0089:10 41          BPL   $00CC

; otherwise immediately turn off the
; disk motor and continue
008B:BD 88 C0       LDA   $C088,X

                 --^--

This is almost certainly the routine
that's waiting for me to insert side A.
First, it's actually waiting for me to
press SPACE, no other keys accepted.
Second, it's checking whether the disk
is write-protected.

On the original disk, side B is write-
protected but side A is not. If I don't
actually flip the disk before pressing
SPACE, the game briefly checks the disk
then refuses to continue. This is how
it knows: by checking whether the disk
in the drive is write-protected. It's a
quick check that doesn't even require
reading the disk; the sensor is built
in to the floppy drive.

Just to be sure, I edited the sector
(ON A BACKUP DISK OBVIOUSLY) and put a
"JMP $FF59" at offset $78, and it
immediately jumped to the monitor after
I pressed SPACE.

[S6,D1=hacked side B with JMP $FF59]
[S5,D1=my work disk]

]PR#6
...loads title screen...
...PLEASE TURN DISK OVER...
...beep...
*C500G
...reboots slot 5...

]4C 59 FF<Ctrl-F>
6178

Wait, what?

Here's what: my work disk runs 4LIVE
<https://github.com/a2-4am/4live>
which is a delightful tool written by
qkumba that allows me to take notes and
annotate them while I'm cracking. It
also adds a full memory search to the
command prompt: just type some hex
bytes and press <Ctrl-F>, and it will
print the addresses that match. Very
useful in exactly this situation, where
I've made changes on disk but don't
know where my code ends up in memory.

Let's go find that protection check.

                   ~

               Chapter 2
       In Which We Zero In On It


]CALL -151

*6178L

; I put this here -- the original game
; had "LDX $B7E9"
6178-   4C 59 FF    JMP   $FF59

; write-protect check
617B-   BD 89 C0    LDA   $C089,X
617E-   A9 00       LDA   #$00
6180-   20 A8 FC    JSR   $FCA8
6183-   BD 8D C0    LDA   $C08D,X
6186-   BD 8E C0    LDA   $C08E,X

; not write-protected -> branch
6189-   10 41       BPL   $61CC

The code from $618E to $61CB clears the
screen, re-prints the same PLEASE TURN
DISK OVER message, plays a few sounds,
and jumps back to wait for a key. We'll
take the branch and continue at $61CC.

; save some bytes to the stack
61CC-   A2 13       LDX   #$13
61CE-   BD EC BC    LDA   $BCEC,X
61D1-   48          PHA
61D2-   CA          DEX
61D3-   10 F9       BPL   $61CE

; copy $8800+ to $B800+
61D5-   A9 88       LDA   #$88
61D7-   85 85       STA   $85
61D9-   A9 B8       LDA   #$B8
61DB-   85 83       STA   $83
61DD-   A0 00       LDY   #$00
61DF-   84 82       STY   $82
61E1-   84 84       STY   $84
61E3-   B1 84       LDA   ($84),Y
61E5-   91 82       STA   ($82),Y
61E7-   C8          INY
61E8-   D0 F9       BNE   $61E3
61EA-   E6 85       INC   $85
61EC-   E6 83       INC   $83
61EE-   A5 83       LDA   $83

; until the target address hits $C000
61F0-   C9 C0       CMP   #$C0
61F2-   D0 EF       BNE   $61E3

; restore bytes from the stack
61F4-   A2 00       LDX   #$00
61F6-   68          PLA
61F7-   9D EC BC    STA   $BCEC,X
61FA-   E8          INX
61FB-   E0 14       CPX   #$14
61FD-   90 F7       BCC   $61F6

DOS 3.3 uses $B800-$BFFF for its RWTS,
the low-level disk reading and writing
routines. Lots of games that don't use
DOS files nevertheless use its RWTS to
read the disk. This is one of those.

Looking at $8800 in memory, it looks
like a standard RWTS -- $8D00 has the
entry point code I would expect at
$BD00, $8944 has the address field code
I would expect at $B944, &c. ($8D4F has
another "LDA $C089,X" that I found in
my earlier search. Again, it's not
suspicious because it's a legitimate
part of the RWTS.)

So we're copying an RWTS into place to
use for reading side A, and the next
few lines appear to set up a multi-
sector read.

; $0A sectors
61FF-   A9 0A       LDA   #$0A
6201-   8D E1 B7    STA   $B7E1

; first sector to read is T22,S0F
; (usually decremented)
6204-   A9 22       LDA   #$22
6206-   8D EC B7    STA   $B7EC
6209-   A9 0F       LDA   #$0F
620B-   8D ED B7    STA   $B7ED

; first sector is stored at $6900
; (usually decremented)
620E-   A9 69       LDA   #$69
6210-   8D F1 B7    STA   $B7F1
6213-   4C 70 B7    JMP   $B770

*B770L

; read the sectors
B770-   20 93 B7    JSR   $B793

; jump to the code we just read
B773-   4C 00 60    JMP   $6000

We could... actually just run that,
minus the final jump to $6000.

But first, let's save the code we've
captured so far.

; not sure how much to save here, but
; disk space is cheap, right?
*BSAVE OBJ.6000-B7FF,A$6000,L$5800

; now disconnect my work disk's DOS
*FE89G FE93G

; RTS instead of JMP $6000
*B773:60

[S6,D1=side A (backup)]

; let the disk read itself
*611CG
...read read read...

; reboot to my work disk
*C500G

]BSAVE OBJ.6000-69FF,A$6000,L$A00
]CALL -151

*6000L

; read one more sector from T04,S0F
; into $6A00
6000-   A9 04       LDA   #$04
6002-   8D EC B7    STA   $B7EC
6005-   A9 0F       LDA   #$0F
6007-   8D ED B7    STA   $B7ED
600A-   A9 01       LDA   #$01
600C-   8D E1 B7    STA   $B7E1
600F-   A9 6A       LDA   #$6A
6011-   8D F1 B7    STA   $B7F1
6014-   20 93 B7    JSR   $B793
...

; clear the hi-res screen (not shown)
605F-   20 A7 68    JSR   $68A7
...

; show the hi-res screen
60D6-   AD 50 C0    LDA   $C050
60D9-   AD 54 C0    LDA   $C054
60DC-   AD 57 C0    LDA   $C057
60DF-   AD 52 C0    LDA   $C052
60E2-   8D 10 C0    STA   $C010
...

; show hi-res page 1 and exit via a
; routine that loads the next phase
; from disk
6118-   AD 54 C0    LDA   $C054
611B-   4C 68 B7    JMP   $B768

I'm pretty sure this is the series of
screens where it asks you to type in
your own message or press Esc to have
the computer choose one for you. The
code at $B768 is loading more from disk
(at $6000, thus clobbering this code)
then jumping to it. To wit:

*B768L

B768-   A2 03       LDX   #$03
B76A-   20 C2 B7    JSR   $B7C2
B76D-   4C 00 60    JMP   $6000

*B7C2L

; X is an index into arrays that set up
; another multi-sector read
B7C2-   BD 83 B7    LDA   $B783,X
B7C5-   8D E1 B7    STA   $B7E1
B7C8-   BD 87 B7    LDA   $B787,X
B7CB-   8D EC B7    STA   $B7EC
B7CE-   BD 8B B7    LDA   $B78B,X
B7D1-   8D ED B7    STA   $B7ED
B7D4-   BD 8F B7    LDA   $B78F,X
B7D7-   8D F1 B7    STA   $B7F1
B7DA-   20 93 B7    JSR   $B793
B7DD-   60          RTS

I'm going to edit my backup copy of
side A to break to the monitor at $611B
instead of jumping to $B768.

T22,S07,$1C: 68B7 -> 59FF

Rebooting side B and flipping to side A
when prompted, I get... nothing. It
never makes it as far as $611B.

Which is great, because it means I'm
zeroing in on the protection check.

                   ~

               Chapter 3
     In Which We Definitely Find It
   But Are Left With Questions About
  Its Seemingly Miraculous Appearance


I spent quite a bit of time bisecting
the code between $6000 and $611B to
identify the call to the protection
check. There's a loop at $6062 that
paints twelve little sprites on the
screen like a clock face. After it
shows the hi-res screen (at $60D6),
we have this series of calls:

60E5-   20 22 61    JSR   $6122
60E8-   20 D2 61    JSR   $61D2
60EB-   20 BA 62    JSR   $62BA
60EE-   20 1C 63    JSR   $631C
60F1-   20 9E 63    JSR   $639E
60F4-   20 A5 64    JSR   $64A5
60F7-   20 93 62    JSR   $6293
60FA-   20 9E 63    JSR   $639E
60FD-   AD 21 61    LDA   $6121
6100-   F0 1C       BEQ   $611E
6102-   20 FB 63    JSR   $63FB
...
611E-   4C E8 60    JMP   $60E8

Almost all of these are conditional
print routines. For example, $6122
might print the "type in your own
message" prompt, if it's time to do
that. $61D2 might print the "press
Return when finished" message that
displays after you type one character.
$62BA might print the "enter your level
of difficulty" message that displays if
you press Esc (to have the game choose
a random message instead of entering
your own). &c.

The whole thing loops back to $60E8
until $6121 is non-zero (checked at
$60FD), at which point it falls through
and calls $63FB, which unconditionally
prints "prepare to enter the digital
dimension." My non-working copy got
that far, so I initially assumed that
the protection check was called from
within that subroutine. However, the
subroutine is mercifully short and
straightforward:

*63FBL

; print "prepare to enter"
63FB-   A2 0A       LDX   #$0A
63FD-   A0 64       LDY   #$64
63FF-   20 6B 68    JSR   $686B

; print "the digital dimension"
6402-   A2 1D       LDX   #$1D
6404-   A0 64       LDY   #$64
6406-   20 6B 68    JSR   $686B

; return to caller
6409-   60          RTS

First I thought, maybe the print
routine at $686B has a sneaky condition
in it that triggers the protection?
I've seen that before on other disks.
But no, it's just the table lookups and
blitting you would expect from a hi-res
character generator.

Then I thought, maybe they're being
REALLY sneaky and modifying the RTS at
$6409, so by the time this subroutine
is called it actually falls through to
a protection check.

To test that theory, I hacked my non-
working copy of side A to jump to the
monitor at $6102, instead of the JSR.

T22,S07,$02: 20FB63 -> 4C59FF

This causes the game to break to the
monitor as expected. But to my chagrin,
the subroutine at $63FB hadn't changed.

But!

Let's take another look at the caller:

*6102L

; originally "JSR $63FB"
; (my copy reached here)
6102-   4C 59 FF    JMP   $FF59
6105-   8D 10 C0    STA   $C010
6108-   AD C6 68    LDA   $68C6
610B-   D0 FB       BNE   $6108
610D-   A2 05       LDX   #$05
610F-   B5 FA       LDA   $FA,X
6111-   29 7F       AND   #$7F
6113-   95 FA       STA   $FA,X
6115-   CA          DEX
6116-   10 F7       BPL   $610F
6118-   20 04 70    JSR   $7004

; originally "JMP $B768"
; (my copy did NOT reach here)
611B-   4C 59 FF    JMP   $FF59

The code at $6102 and $611B are the two
instructions I replaced with jumps to
the monitor. But wait, what's this at
$6118?

6118-   20 04 70    JSR   $7004

That... was not there before. I mean, I
literally saved this chunk of memory to
my work disk earlier, and it was this:

6118-   AD 54 C0    LDA   $C054
611B-   4C 68 B7    JMP   $B768

And now it is... not that.

I am suddenly VERY interested to learn
what ends up at $7004.

*7004L

; track 4
7004-   A9 04       LDA   #$04
7006-   8D EC B7    STA   $B7EC
7009-   AE F4 B7    LDX   $B7F4
700C-   8E 03 70    STX   $7003

; seek command
700F-   A2 00       LDX   #$00
7011-   8E F4 B7    STX   $B7F4
7014-   8E 01 70    STX   $7001

; Death Counter?
7017-   A0 04       LDY   #$04
7019-   8C 00 70    STY   $7000

; seek to track 4
701C-   A9 B7       LDA   #$B7
701E-   A0 E8       LDY   #$E8
7020-   20 B5 B7    JSR   $B7B5

; turn on drive motor manually
; (highly suspicious -- also, notice
; that it uses STA $C089,X instead of
; LDA $C089,X, which works just as
; well but evaded my earlier search for
; the more common variant)
7023-   AE F7 B7    LDX   $B7F7
7026-   9D 89 C0    STA   $C089,X

; initially puzzling, but it appears
; that this RWTS keeps track of the
; current track number in $BCF0 instead
; of the usual screen hole in the text
; page
7029-   A0 08       LDY   #$08
702B-   8C F0 BC    STY   $BCF0

702E-   C8          INY
702F-   C8          INY
7030-   98          TYA
7031-   48          PHA

; Standard DOS seek routine takes a
; half-track (a.k.a. "phase") in A.
; Y was 8, incremented twice = 10,
; transferred to A, so we're seeking to
; track 10/2 = 5
7032-   20 A0 B9    JSR   $B9A0

; read the next available address field
7035-   20 44 B9    JSR   $B944
7038-   B0 0B       BCS   $7045

; read the data field
703A-   20 DC B8    JSR   $B8DC
703D-   B0 06       BCS   $7045

; check the track number (saved in zero
; page by the address field parser at
; $B944, which we just called)
703F-   A9 05       LDA   #$05
7041-   C5 2E       CMP   $2E

; track = 5 -> good, branch ahead
7043-   F0 18       BEQ   $705D

; track != 5 -> bad, push a bogus
; address to the stack and crash
; (we may also end up here from $7038
; or $703D, if we didn't successfully
; find and parse an address field or
; a data field)
7045-   A9 C6       LDA   #$C6
7047-   48          PHA
7048-   A9 03       LDA   #$03
704A-   48          PHA
704B-   A9 FF       LDA   #$FF
704D-   8D 01 70    STA   $7001
7050-   60          RTS
...

; pull phase that we pushed earlier
; (currently #$0A)
705D-   68          PLA
705E-   A8          TAY
705F-   C0 0C       CPY   #$0C
7061-   D0 CC       BNE   $702F

We're branching back a few times, but
if you look closely, you'll see that
we're branching back to the second INY
instruction, not the first. That means
A will be #$0B the second time we call
$B9A0. So we're doing everything again,
but on track 5.5 instead.

Then we'll branch back a third time,
but Y will be incremented once more and
A will be #$0C going into $B9A0. So
we'lll do everything once again, but on
track 6 instead.

This is the heart of the protection: an
extra wide track that spans tracks 5,
5.5, and 6. As we saw earlier in the
nibble editor, all the sectors on track
5.5 and track 6 claim to be track 5.
That would be impossible to replicate
with a standard bit copier.

Funnily enough, this protection check
always passes on the first iteration,
because it's on track 5 and reading
sectors and checking that they claim to
be on track 5. Which, even on my copy,
is true. But then the loop continues
with tracks 5.5 and track 6, and it all
falls apart because my track 6 sectors
claim to be track 6 because they are
not psychopaths.

Continuing from $7063...

; decrement counter (initially 4, set
; at $7019)
7063-   CE 00 70    DEC   $7000

; seek to track 4 again
7066-   A9 08       LDA   #$08
7068-   20 A0 B9    JSR   $B9A0

; do it all again, repeatedly
706B-   AC 00 70    LDY   $7000
706E-   D0 A9       BNE   $7019

So this check has to pass 4 times in a
row, or it's off to The Badlands.

; store flag (maybe checked later?)
7070-   8C 02 70    STY   $7002

; turn off drive motor
7073-   9D 88 C0    STA   $C088,X

; restore RWTS parameter table
7076-   AD 03 70    LDA   $7003
7079-   8D F4 B7    STA   $B7F4

; one final check (The Badlands sets
; this address to #$FF, so I guess even
; if you manage to escape from there,
; we want to make extra sure you never
; return from this protection check)
707C-   AD 01 70    LDA   $7001
707F-   F0 03       BEQ   $7084
7081-   4C 00 C6    JMP   $C600

; and finally return to the caller
7084-   60          RTS

Whew.

Now, where in the world did this copy
protection routine... come from?

                   ~

               Chapter 4
      In Which The Code Is Coming
         From Inside The House


First of all, it was not loaded from
disk. I captured everything that was
loaded from disk (OBJ.6000-B7FF), and
$7000 was something else altogether:

*BLOAD OBJ.6000-B7FF
*7000L

7000-   A9 00       LDA   #$00
7002-   8D F0 03    STA   $03F0
7005-   A9 C6       LDA   #$C6
7007-   8D F1 03    STA   $03F1
700A-   A9 0C       LDA   #$0C
700C-   8D E1 B7    STA   $B7E1
700F-   A9 0A       LDA   #$0A
7011-   8D EC B7    STA   $B7EC
7014-   A9 0F       LDA   #$0F
7016-   8D ED B7    STA   $B7ED
7019-   A9 A0       LDA   #$A0
701B-   8D F1 B7    STA   $B7F1
701E-   20 93 B7    JSR   $B793

That's just part of the previous phase
of the boot. It bears no resemblance to
the copy protection routine that ended
up at $7004.

The short answer is, everywhere. The
more I look, the more I find. There is
no part of this code that is not
involved, in some small way, in
constructing this protection routine
out of thin air.

Remember the code at $6000, which read a
sector from track $04? Here's the
"harmless" code immediately after that:

*BLOAD OBJ.6000-69FF
*6017L

6017-   A9 00       LDA   #$00
6019-   85 F8       STA   $F8

; store #$00 in two addresses
; within the protection routine
601B-   8D 1A 70    STA   $701A   <-- !
601E-   8D 6C 70    STA   $706C   <-- !
6021-   A2 05       LDX   #$05

; store #$05 in one address
; within the protection routine
6023-   8E 40 70    STX   $7040   <-- !

; store #$A0 in several zero page
; addresses, and also several addresses
; within the protection routine
; (this is the LDY opcode)
6026-   A9 A0       LDA   #$A0
6028-   95 FA       STA   $FA,X
602A-   CA          DEX
602B-   10 FB       BPL   $6028
602D-   8D 17 70    STA   $7017   <-- !
6030-   8D 33 70    STA   $7033   <-- !
6033-   8D 69 70    STA   $7069   <-- !
6036-   8D 1E 70    STA   $701E   <-- !
6039-   8D 29 70    STA   $7029   <-- !

603C-   A9 02       LDA   #$02
603E-   85 24       STA   $24
6040-   4A          LSR

; store #$01 in one address
; within the protection routine
6041-   8D 4E 70    STA   $704E   <-- !

; store #$00 in several zero page
; addresses, and also one address
; within the protection routine
6044-   A9 00       LDA   #$00
6046-   85 11       STA   $11
6048-   85 22       STA   $22
604A-   85 23       STA   $23
604C-   8D 54 70    STA   $7054   <-- !

; store #$20 in several addresses
; within the protection routine
; (this is the JSR opcode)
604F-   A9 20       LDA   #$20
6051-   8D 32 70    STA   $7032   <-- !
6054-   8D 3A 70    STA   $703A   <-- !
6057-   85 25       STA   $25
6059-   8D 68 70    STA   $7068   <-- !

I thought nothing of these at the time
(who cares about initializing addresses
to zero? or any other constant?) but
know we know better. Now we know that an
important routine ends up in memory at
$7004..$7084. And this is how it gets
there: literally one byte at a time.

Here is the routine that conditionally
prints the "type in your own message"
prompt:

*6122L

6122-   AD 35 64    LDA   $6435
6125-   D0 01       BNE   $6128
6127-   60          RTS
6128-   C9 02       CMP   #$02
612A-   D0 78       BNE   $61A4
612C-   AA          TAX
612D-   E8          INX

; store #$03 in several places
; within the protection routine
612E-   8E 80 70    STX   $7080   <-- !
6131-   8E 0D 70    STX   $700D   <-- !
6134-   8E 77 70    STX   $7077   <-- !
6137-   8E 49 70    STX   $7049   <-- !
613A-   E8          INX

; store #$04 in several places
; within the protection routine
613B-   8E 18 70    STX   $7018   <-- !
613E-   8E 05 70    STX   $7005   <-- !

; actually print the prompt
6141-   A2 CF       LDX   #$CF
6143-   A0 64       LDY   #$64
6145-   20 6B 68    JSR   $686B
6148-   A2 EE       LDX   #$EE
614A-   A0 64       LDY   #$64
614C-   20 6B 68    JSR   $686B

Oh, remember I mentioned "a loop at
$6062 that paints twelve little sprites
on the screen like a clock face"
COMPLETELY CASUALLY, IN PASSING, LIKE
IT WAS NOTHING. The sprite painting
routine starts at $6708, but it doesn't
end until it passes through this code
at $67ED:

*67EDL

67ED-   A9 E5       LDA   #$E5
67EF-   8D 1F 61    STA   $611F
67F2-   A9 04       LDA   #$04
67F4-   8D 19 61    STA   $6119   <-- !
67F7-   A9 60       LDA   #$60
67F9-   8D 20 61    STA   $6120
67FC-   A9 70       LDA   #$70
67FE-   8D 1A 61    STA   $611A   <-- !
6801-   AD E5 60    LDA   $60E5
6804-   8D 18 61    STA   $6118   <-- !

$60E5 is #$20, so we've put "20 04 70"
at $6118, a.k.a. "JSR $7004", the call
to the protection routine.

But wait, there's more.

6807-   AD EB 62    LDA   $62EB  ; =$70
680A-   85 03       STA   $03
680C-   A9 00       LDA   #$00
680E-   85 02       STA   $02

; copy $52 bytes into various addresses
; in the $7000 page, using an index
; table to find the offset of each byte
6810-   A2 52       LDX   #$52
6812-   BD 62 66    LDA   $6662,X
6815-   BC B5 66    LDY   $66B5,X
6818-   91 02       STA   ($02),Y
681A-   CA          DEX
681B-   10 F5       BPL   $6812
681D-   60          RTS

And that's how you build a protection
routine out of thin air.

                   ~

               Chapter 5
       In Which We Are Not Amused


Now that we've... uncovered? Revealed?
what's the word to describe seeing what
was right in front of your face the
whole time?

Anyway, now that we've done that, we
should be able to bypass the protection
check by changing the instruction that
sets up the call to $7004. $6801 stores
#$20 at $6118 (the JSR opcode), but if
we put #$AD there instead, "JSR $7004"
turns into a harmless "LDA $7004" and
execution will continue.

[S6,D1=fresh copy of side A]

; LDA $60E5 -> LDA #$AD / NOP
T22,S0E,$01: ADE560 -> A9ADEA

Rebooting side B, flipping to side A
when prompted, "preparing to enter the
digital dimension," and...

Success! The first level's background
graphic loads, and the game begins.

Except...

Willy Byte is supposed to make his
grand entrance by falling from a chute
in the ceiling; his parachute opens,
and he lands safely on the playing
field near the bottom of the screen.
But his parachute never opens, and he
falls through the bottom of the screen
and reappears at the top, endlessly.

Ha ha! Amusing in a quiet way, said
Eeyore, but not really helpful.

                   ~

               Chapter 6
     Two Holes Are Better Than One;
      Any Mouse Will Tell You That


After (not) calling the protection
check at $7004, execution continues at
$B768, which looks like this:

B768-   A2 03       LDX   #$03
B76A-   20 C2 B7    JSR   $B7C2
B76D-   4C 00 60    JMP   $6000

I can hack my non-working copy to jump
to the monitor at $B76D instead of
continuing to $6000. This code is on
side B, all the way back on track 0.

[S6,D1=copy of side B]

T00,S01,$6E: 0060 -> 59FF

Rebooting side B, switching to side A,
and attempting to start level 1, and...

<beep>

*

Success! I've broken into the monitor
with the level 1 game code in memory.

...Where, exactly?

Going back to $B768, it calls $B7C2
with X=3. Here's $B7C2 (still in
memory):

*B7C2L

B7C2-   BD 83 B7    LDA   $B783,X
B7C5-   8D E1 B7    STA   $B7E1
B7C8-   BD 87 B7    LDA   $B787,X
B7CB-   8D EC B7    STA   $B7EC
B7CE-   BD 8B B7    LDA   $B78B,X
B7D1-   8D ED B7    STA   $B7ED
B7D4-   BD 8F B7    LDA   $B78F,X
B7D7-   8D F1 B7    STA   $B7F1
B7DA-   20 93 B7    JSR   $B793
B7DD-   60          RTS

We're setting up RWTS parameters for a
multi-sector read. Checking the third
byte of each array, the final parameter
table looks like this:

$B786 -> $B7E1 = $28 (sector count)
$B78A -> $B7EC = $14 (start track)
$B78E -> $B7ED = $02 (start sector)
$B792 -> $B7F1 = $87 (start address)

Which means we read $28 sectors into
$6000..$87FF.

; reboot to my work disk
*C500G

; save newly loaded game code
]BSAVE OBJ.6000-87FF,A$6000,L$2800

Of note: this completely clobbers the
previous protection check (at $7004+),
that check's success flag (at $7002),
and its caller (at $6000+). The game is
not failing because it noticed that we
bypassed the previous protection check;
it's failing for some other reason.

Like a second protection check.

Unfortunately (for me), there is no
hard failure in this second protection
check. I can't bisect the code to find
it, because all it does is set some
global flag that the game checks later.

But sometimes you get lucky.

On the theory that there is only one
structural protection on this disk (the
extra wide track that spans tracks 5,
5.5, and 6), the second protection
check probably looks similar to the
first. Of course, the first check was
constructed byte by byte in memory, but
maybe...

]CALL -151

*20 A0 B9<Ctrl-F>
8173
8194

Aha! 4LIVE's memory search function to
the rescue! (Thanks, qkumba.)

These are both part of the same routine
starting at $8148. ($8147 is an RTS.)

*8148L

; seek to track 4
8148-   A9 04       LDA   #$04
814A-   8D EC B7    STA   $B7EC
814D-   AD F4 B7    LDA   $B7F4
8150-   8D A7 81    STA   $81A7
8153-   A2 00       LDX   #$00
8155-   8E F4 B7    STX   $B7F4
8158-   A0 01       LDY   #$01
815A-   8C A6 81    STY   $81A6
815D-   A9 B7       LDA   #$B7
815F-   A0 E8       LDY   #$E8
8161-   20 B5 B7    JSR   $B7B5

; turn on drive motor
8164-   AE F7 B7    LDX   $B7F7
8167-   9D 89 C0    STA   $C089,X
816A-   A0 08       LDY   #$08
816C-   8C F0 BC    STY   $BCF0
816F-   C8          INY
8170-   C8          INY
8171-   98          TYA
8172-   48          PHA

; seek to track 5 (later 5.5 and 6)
8173-   20 A0 B9    JSR   $B9A0

; read and parse address field
8176-   20 44 B9    JSR   $B944
8179-   B0 0B       BCS   $8186

; read and verify data field
817B-   20 DC B8    JSR   $B8DC
817E-   B0 06       BCS   $8186

; check if track number = 5
8180-   A9 05       LDA   #$05
8182-   C5 2E       CMP   $2E

; yes -> branch over next instruction
8184-   F0 03       BEQ   $8189

; no -> set global flag (aha!)
8186-   EE 6E 69    INC   $696E

; continue for tracks 5.5 and 6
8189-   68          PLA
818A-   A8          TAY
818B-   C0 0C       CPY   #$0C
818D-   D0 E1       BNE   $8170

; decrement counter, but it was
; initialized to 1 (at $815A) so we
; only bother doing this protection
; check once
818F-   CE A6 81    DEC   $81A6

; reset disk to track 4
8192-   A9 08       LDA   #$08
8194-   20 A0 B9    JSR   $B9A0

; this actually never branches
8197-   AC A6 81    LDY   $81A6
819A-   D0 BE       BNE   $815A

; turn off drive motor
819C-   9D 88 C0    STA   $C088,X

; restore RWTS parameters we changed
819F-   AD A7 81    LDA   $81A7
81A2-   8D F4 B7    STA   $B7F4

; exit to caller regardless of success
; or failure
81A5-   60          RTS

I was right: there is a second check,
and it does set a global flag ($696E)
on failure.

Searching both sides of the disk for
"F0 03 EE 6E 69" (from $8184), I find
this protection check on track $13. I
should be able to put an RTS at the
beginning of the routine to bypass it.

[S6,D1=non-working copy of side A]
T13,S0C,$48: A9 -> 60

Rebooting side B, switching to side A,
and attempting to start level 1, and...

Willy Byte never appears at all, and
there are graphical glitches flashing
along the side of the screen.

Oh bother, said Pooh. Oh help and
bother.

                   ~

               Chapter 7
      Mammas Don't Let Your Babies
     Grow Up To Build Interpreters


Disabling the first protection check at
$7004 (by not calling it) worked. I'm
confident that there are no lingering
side effects that could be detected.

Disabling the second protection check
at $8148 (by putting an RTS at $8148)
did not work. This check has no side
effects unless it fails, so either
there is a THIRD protection check, or
there is some sort of tamper check
elsewhere in the code that is detecting
that I disabled the protection check at
$8148.

Or both.

; reboot to my work disk
]PR#5

; reload level 1 game code
]BLOAD OBJ.6000-87FF
]CALL -151

; search for references to $8148
*48 81<Ctrl-F>
74D2

Aha! There is one reference to $8148
elsewhere in memory.

*74D1L

74D1-   AD 48 81    LDA   $8148
74D4-   C9 A9       CMP   #$A9
74D6-   D0 78       BNE   $7550

Damn it. They anticipated my approach.

74D8-   CD 5D 81    CMP   $815D
74DB-   D0 71       BNE   $754E

Double damn it.

74DD-   CD 80 81    CMP   $8180
74E0-   D0 6A       BNE   $754C

Triple.

74E2-   CD 92 81    CMP   $8192
74E5-   D0 63       BNE   $754A

Quadruple.

That's all the comparisons I found; the
rest of the routine does something
unrelated. Which is good, because I'm
running out of curse words, and I don't
know what comes after "quadruple."

I bypassed the first protection check
by changing the calling code, not the
protection code. Maybe I should do that
here. Then the tamper check would pass,
since the copy protection code would
not, in fact, have been tampered with.
It just wouldn't get called, which is
fine because it has no side effects.

Unfortunately, I already searched for
references to $8148 in memory, and all
I found was the tamper check at $74D1.
So how does this protection check ever
get called?

Theory: the game may be pushing the
address to the stack directly in order
to "return" to it later. I've seen this
sort of obfuscation on other disks.

; search memory for "LDA #$81"
*A9 81<Ctrl-F>
69CE
7857

Let's look at $69CE first.

*69CEL

69CE-   A9 81       LDA   #$81
69D0-   8D AC 6E    STA   $6EAC

That seems harmless enough on its own,
until you look at $6EAC and see that
it's executable code.

6EA6-   C6 92       DEC   $92
6EA8-   F0 05       BEQ   $6EAF
6EAA-   A9 01       LDA   #$01
6EAC-   8D E7 84    STA   $84E7   <-- ?
6EAF-   60          RTS

#$81 is a valid 6502 opcode, but it's
not a very widely used one. (It stores
the accumulator in an indirect address
pointed to by a zero page address which
is itself indexed by X, which is mostly
useful if you're writing an interpreter
or you're trying to obfuscate something
else. Dear God, please don't let it be
an interpreter.)

Unless...

Maybe we're not looking for stack
manipulation at all. Maybe we've (once
again) stumbled on part of a multi-
faceted campaign to construct the code
that calls a protection check.

; look for references to $6EAB
*AB 6E<Ctrl-F>
8604

Looking at surrounding code, it appears
that this is part of a subroutine that
starts at $85F2. ($85F1 is an RTS.)

*85F2L

85F2-   A9 20       LDA   #$20
85F4-   85 03       STA   $03
85F6-   A9 40       LDA   #$40
85F8-   85 01       STA   $01
85FA-   A9 00       LDA   #$00
85FC-   85 02       STA   $02
85FE-   85 00       STA   $00
8600-   A8          TAY
8601-   A9 48       LDA   #$48
8603-   8D AB 6E    STA   $6EAB   <-- !
8606-   B1 02       LDA   ($02),Y
8608-   91 00       STA   ($00),Y
860A-   C8          INY
860B-   D0 F9       BNE   $8606
860D-   E6 01       INC   $01
860F-   A5 03       LDA   $03
8611-   18          CLC
8612-   69 01       ADC   #$01
8614-   85 03       STA   $03
8616-   C9 40       CMP   #$40
8618-   D0 EC       BNE   $8606
861A-   60          RTS

This routine copies hi-res page 2
($4000) to hi-res page 1 ($2000). Which
is fine, except in the middle it sets
$6EAB to #$48 for absolutely no reason.

I bet...

; search memory for references to $6EAA
*AA 6E<Ctrl-F>
76C5

*76C1L

76C1-   A9 26       LDA   #$26
76C3-   0A          ASL
76C4-   8D AA 6E    STA   $6EAA

Oh that's clever. #$26 shifted left is
#$4C, the JMP opcode. (This is, again,
stuck in the middle of unrelated code.)
Assuming that all of these routines are
executed at some point, $6EAA ends up
being "4C 48 81", a.k.a. "JMP $8148",
a.k.a. "jump to the second protection
check."

The RTS opcode is #$60, shifting right
gives me #$30. I should be able to
bypass the jump to the protection check
by changing $76C2 to #$30.

; undo my RTS patch at $8148
T13,S0C,$48: 60 -> A9

; change code that constructs the JMP
; opcode at $6EAA so it constructs an
; RTS instead
T13,S01,$C2: 26 -> 30

Now the protection check itself is
unmodified, which means the tamper
check at $74D1 shouldn't complain. And
the code that constructs a JMP to the
protection check at $8148 now puts an
RTS there instead.

Rebooting side B, switching to side A,
and attempting to start level 1, and...

The game reboots.

I am not enjoying this nearly as much
as you might think.

                   ~

               Chapter 8
          In Which We Give It
          Everything We've Got


Disabling the second protection check
at $8148 (by modifying the code that
builds the caller at runtime) did not
work. This check has no side effects
unless it fails, so either there is a
THIRD protection check, or there is a
SECOND tamper check detecting that I
modified the code that builds the code
that calls the protection check.

At this point, neither option would
surprise me.

Nor both.

]PR#5
]BLOAD OBJ.6000-87FF
]CALL -151

; search memory for references to $76C2
; (the byte I modified)
*C2 76<Ctrl-F>
63F9

*63F1L

63F1-   AD C1 76    LDA   $76C1
63F4-   C9 A9       CMP   #$A9
63F6-   D0 F0       BNE   $63E8
63F8-   AD C2 76    LDA   $76C2
63FB-   C9 26       CMP   #$26
63FD-   D0 E9       BNE   $63E8
63FF-   AD C3 76    LDA   $76C3
6402-   C9 0A       CMP   #$0A
6404-   D0 E2       BNE   $63E8
6406-   AD C4 76    LDA   $76C4
6409-   C9 8D       CMP   #$8D
640B-   D0 DB       BNE   $63E8
640D-   AD C5 76    LDA   $76C5
6410-   C9 AA       CMP   #$AA
6412-   D0 D4       BNE   $63E8
6414-   AD C6 76    LDA   $76C6
6417-   C9 6E       CMP   #$6E
6419-   D0 CD       BNE   $63E8
641B-   60          RTS

ARE YOU KIDDING ME?!?

They are explicitly checking that no
one has tampered with the six bytes of
code responsible for putting a single
JMP opcode into place to call the
protection check that I can't change
because of a different tamper check.

QUINTUPLE DAMN IT. (I looked it up.)

This is as good a time as any to tell
you that I also looked at $7857 the
second match for "LDA #$81". (The first
match was $69CE, which built the JMP
$8148 at $6EAA.) I am happy -- nay,
thrilled -- to report that $7857 is
part of a THIRD tamper check.

*7853L

; set up ($02) to point to $8148
; (the second protection check)
7853-   A9 48       LDA   #$48
7855-   85 02       STA   $02
7857-   A9 81       LDA   #$81
7859-   85 03       STA   $03

; check $31 different nonsequential
; bytes of the protection check against
; an array of expected values
785B-   A2 31       LDX   #$31
785D-   BD 70 78    LDA   $7870,X
7860-   BC F9 69    LDY   $69F9,X
7863-   D1 02       CMP   ($02),Y

; if any fails, branch to a BRK (which
; crashes and reboots)
7865-   D0 05       BNE   $786C
7867-   CA          DEX
7868-   10 F3       BPL   $785D
786A-   60          RTS
786B-   C9 00       CMP   #$00  <-- BRK
786D-   D0 EE       BNE   $785D
786F-   60          RTS

The only reason this tamper check
didn't trigger a crash when I put an
RTS at $8148 is that that's not one of
the $31 bytes it checks (presumably
because the author knows that $8148 is
checked elsewhere). It was just dumb
luck that I tripped that tamper check
instead of this one.

Thus...

We shall disable the protection code

                  AND

the tamper check on the protection code

                  AND

the code that builds the code that
calls the protection code

                  AND

the tamper check on that code

   AND THEN THEY'LL ALL BE SORRY. (*)

(*) not guaranteed, actual sorrow may
    vary

Here we go.

; disable protection check at $8148
; (again)
T13,S0C,$48: A9 -> 60

; change code at $76C1 that constructs
; the JMP at $6EAA so it constructs an
; RTS instead
T13,S01,$C2: 26 -> 30

; disable failure branches in $74D1
; tamper check
T12,S0F,$D7: 78 -> 00
T12,S0F,$DC: 71 -> 00
T12,S0F,$E1: 6A -> 00
T12,S0F,$E6: 63 -> 00

; disable failure branch in $7853
; tamper check
T13,S03,$65: 05 -> 00

Rebooting side B, switching to side A,
and attempting to start level 1, and...

The game plays!

...until level 2, when it crashes.

                   ~

               Chapter 9
         In Which We Start Over


Again, I don't know what's going on.
Either there is a FOURTH tamper check
that has detected one of the multitude
of changes I've made to disable the
second protection check, or there is a
third protection check.

Or, you know, both. At this point, no
reasonable person would bet against the
"both."

On the hunch that there is a third
protection check, and that it is similar
to the first two protection checks, I
returned to my trusty sector editor and
searched for "JSR $B9A0", the call to
the track seek routine. Side B had no
matches (is there anything from side B
still in memory, besides the RWTS?) but
side A is a different story.

                 --v--

[$20 $A0 $B9]
------------- DISK SEARCH -------------

$13/$0C-$73   $13/$0C-$94   $17/$01-$1A
$17/$01-$39   $1A/$08-$09   $1A/$08-$28

                 --^--

I know about the code on track $13;
that's the second protection check at
$8148. Here's the code on track $17
(actually starts on sector 0):

[T17,S00]
----------- DISASSEMBLY MODE ----------
00EF:A9 04          LDA   #$04
00F1:8D EC B7       STA   $B7EC
00F4:AD F4 B7       LDA   $B7F4
00F7:8D 4C 81       STA   $814C
00FA:A2 00          LDX   #$00
00FC:8E F4 B7       STX   $B7F4
00FF:A0 02          LDY   #$02
...
[T17,S01]
0001:8C 4B 81       STY   $814B
0004:A9 B7          LDA   #$B7
0006:A0 E8          LDY   #$E8
0008:20 B5 B7       JSR   $B7B5
000B:AE F7 B7       LDX   $B7F7
000E:9D 89 C0       STA   $C089,X
0011:A0 08          LDY   #$08
0013:8C F0 BC       STY   $BCF0
0016:C8             INY
0017:C8             INY
0018:98             TYA
0019:48             PHA
001A:20 A0 B9       JSR   $B9A0
001D:20 44 B9       JSR   $B944
0020:B0 0B          BCS   $002D
0022:20 DC B8       JSR   $B8DC
0025:B0 06          BCS   $002D
0027:A9 05          LDA   #$05
0029:C5 2E          CMP   $2E
002B:F0 01          BEQ   $002E
002D:00             BRK           <-- !
002E:68             PLA
002F:A8             TAY
0030:C0 0C          CPY   #$0C
0032:D0 E3          BNE   $0017
0034:CE 4B 81       DEC   $814B
0037:A9 08          LDA   #$08
0039:20 A0 B9       JSR   $B9A0
003C:AC 4B 81       LDY   $814B
003F:D0 C0          BNE   $0001
0041:9D 88 C0       STA   $C088,X
0044:AD 4C 81       LDA   $814C
0047:8D F4 B7       STA   $B7F4
004A:60             RTS

                 --^--

I could change the BRK at offset $2D to
NOP and it would fall through, but I
bet it's tamper checked somewhere. Or I
could disable the caller; based on the
absolute address it uses for temporary
storage ($814B), this code is loaded at
$80EF.

Searching both sides for "EF 80" finds
several matches:

                 --v--

[$EF $80]
------------- DISK SEARCH -------------

$15/$00-$F3   $17/$00-$C9   $17/$0A-$6F

                 --^--

So there are (at least) three callers
to the third protection check. I did
not look for tamper checks.

The FOURTH protection check is on track
$1A. I won't list it, but it starts at
T1A,S07,$DE and continues to sector 8.
It works exactly the same way as the
first, second, and third checks. It is
loaded at $73DE. It is called via JSR
at T1A,S09,$1B. I did not look for
tamper checks.

Dear reader, I am tired of this game.

Not the game itself, which is lovely.
I'm tired of the meta-game, wherein a
skilled developer -- decades hence --
put a whole bunch of thought into how
their copy protection would run and how
other people would try to break it, and
came up with a masterful combination of

- obfuscation: building code and
  callers at runtime
- redundancy: 4+ protection checks,
  7+ calls, many delayed
- paranoia: 3+ tamper checks, come on

I'm tired of this game because I'm
losing.

                   ~

               Chapter 10
      In Which We Change The Game


There were other companies renowned for
their copy protection. Electronic Arts
famously used an extra wide track, much
like this disk. They also invented an
entire virtual machine and interpreter
to execute their protection routines...
and their tamper checks. (See 4am crack
no. 1033 "Pinball Construction Set" for
all that.)

This disk does not go quite that far.
Besides being written in 6502 assembly
(whew), there is another significant
difference. The protection checks in
EA's games manipulated low-level drive
softswitches directly to move the drive
head and read the disk. But all of the
protection checks on this disk use the
standard DOS 3.3 RWTS routines to seek
to a track, parse an address field, &c.

The DOS 3.3 RWTS is, in essence, an
abstraction. Call this routine and the
drive head will move to a given track.
Call this routine and it will find and
parse the next available address field.
Granted, that's still pretty low level,
but it gives me an opening.

What if the track seek routine... lied?

Hear me out. The way each protection
check is structured, it succeeds on the
first iteration, even on a non-original
disk. It seeks to track 5 and checks
that a sector claims to be on track 5.
It's only the second and third times
through, after it seeks to track 5.5 or
6, that the check fails.

What if the track seek routine at $B9A0
just... didn't always seek to the track
you requested?

Crucially, none of the four protection
routines verify, after calling $B9A0,
that the drive actually seeked to the
track they asked for. If we can keep
the drive on track 5, instead of moving
to track 5.5 or 6, the protection check
will loop three times and succeed every
time and think it has an original disk
WITHOUT ANY CHANGES TO THE PROTECTION
CODE AT ALL.

And we CAN keep the drive on track 5,
because the vital function of "seeking
to a track" has been outsourced to the
RWTS routine at $B9A0.

And I bet they're not tamper-checking
the RWTS.

Let's find out.

The track seek routine for side A
(remember, side A has its own RWTS for
some reason -- it's copied from $8800+
after you flip the disk) starts like
this:

B9A0-   86 2B       STX   $2B
B9A2-   85 2A       STA   $2A
B9A4-   CD F0 BC    CMP   $BCF0
B9A7-   F0 53       BEQ   $B9FC

I'm going to patch the first four bytes
so it calls a routine at $BA69 instead:

B9A0-   4A          LSR             <--
B9A1-   20 69 BA    JSR   $BA69     <--
B9A4-   CD F0 BC    CMP   $BCF0
B9A7-   F0 53       BEQ   $B9FC

$BA69 is a small range of unused space
within the RWTS. Later versions of DOS
used it for patches, but this game only
uses the RWTS, not the rest of DOS.

Here's what I put at $BA69:

; Because of the LSR at $B9A0, the
; accumulator is now the actual (whole)
; track number, not a phase.
; We check if we've been asked to seek
; to a track that we know is only used
; by the protection checks.
BA69-   C9 06       CMP   #$06
BA6B-   D0 02       BNE   $BA6F

; if so, move to track 5 instead
BA6D-   A9 05       LDA   #$05

; shift to turn the track into a phase
; again (the rest of the seek routine
; expects this)
BA6F-   0A          ASL

; replicate the original code at $B9A0
BA70-   86 2B       STX   $2B
BA72-   85 2A       STA   $2A

; return to caller (execution will
; continue at $B9A4)
BA74-   60          RTS

In essence, this RWTS selectively lies.
If you ask to seek to track 4, or 7, or
23, or almost anything, it will do it.
But if you ask to seek to track 6, it
will actually move to track 5.

Additionally, if you ask to seek to ANY
half track (like 5.5), it will actually
move to the whole track before it (like
5). Then it will return gracefullly and
claim it succeeded.

Remember, none of the protection checks
verify the track after seeking.

The RWTS for side A is on track $10 of
side B. Starting with a fresh copy of
both sides of the disk, I made these
changes to side B:

T10,S01,$A0: 862B852A -> 4A2069BA
T10,S02,$69 -> C906D002A9050A862B852A60

I made no changes whatsoever to side A.

Rebooting side B, switching to side A,
and attempting to start level 1, and...

The game plays.

On to level 2, and...

The game plays.

On to level 3, and...

The game plays.

&c.

All copies of the protection code (at
least 4) remain intact.

All calls to the protection code (at
least 7) remain intact.

All the code that builds the code that
calls the other code, remains intact.

All the code that checks that the code
that builds the code that calls the
other code, remains intact. In fact,
none of the tamper checks scattered
throughout the game code ever trip. Why
would they? None of the game code has
been tampered with.

All protection routines are executed as
expected, and they all pass. They're
just no longer checking what they think
they're checking. They had one fatal
flaw: they entrusted a vital function
(track seek) to an untrusted RWTS. When
that RWTS started lying, they continued
to trust it. And all the checks upon
checks upon checks were rendered moot.

Quod erat liberandum.

                   ~

            Acknowledgements


Many thanks to Murray Krehbiel -- the
original game developer AND protection
developer -- for sending me a boxed
retail copy of his game to preserve and
document.

Also thanks to qkumba for playtesting
several incomplete versions of this
crack, plus the final working version.

---------------------------------------
A 4am crack                    No. 2698
------------------EOF------------------
